expander 2.2.1

Expands proc macro output to a file, to enable easier debugging
Documentation

crates.io CI commits-since rust 1.65.0+ badge

expander

Expands a proc-macro into a file, and uses a include! directive in place.

Advantages

  • Only expands a particular proc-macro, not all of them. I.e. tracing is notorious for expanding into a significant amount of boilerplate with i.e. cargo expand
  • Get good errors when your generated code is not perfect yet

Usage

In your proc-macro, use it like:


#[proc_macro_attribute]
pub fn baz(_attr: proc_macro::TokenStream, input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    // wrap as per usual for `proc-macro2::TokenStream`, here dropping `attr` for simplicity
    baz2(input.into()).into()
}


 // or any other macro type
fn baz2(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
    let modified = quote::quote!{
        #[derive(Debug, Clone, Copy)]
        #input
    };

    let expanded = Expander::new("baz")
        .add_comment("This is generated code!".to_owned())
        .fmt(Edition::_2021)
        .verbose(true)
        // common way of gating this, by making it part of the default feature set
        .dry(cfg!(feature="no-file-expansion"))
        .write_to_out_dir(modified.clone()).unwrap_or_else(|e| {
            eprintln!("Failed to write to file: {:?}", e);
            modified
        });
    expanded
}

will expand into

include!("/absolute/path/to/your/project/target/debug/build/expander-49db7ae3a501e9f4/out/baz-874698265c6c4afd1044a1ced12437c901a26034120b464626128281016424db.rs");

where the file content will be

#[derive(Debug, Clone, Copy)]
struct X {
    y: [u8:32],
}

Exemplary output

An error in your proc-macro, i.e. an excess ;, is shown as



becomes



which shows exactly where in the generated code, the produce of your proc-macro, rustc found an invalid token sequence.

Now this was a simple example, doing this with macros that would expand to multiple tens of thousand lines of code when expanded with cargo-expand, and still in a few thousand that your particular one generates, it's a life saver to know what caused the issue rather than having to use eprintln! to print a unformated string to the terminal.

Hint: You can quickly toggle this by using .dry(true || false)

Features

Special handling: syn

By default expander is built with feature syndicate which adds fn maybe_write_* to struct Expander, which aids handling of Result<TokenStream, syn::Error> for the commonly used rust parsing library syn.

Reasoning

syn::Error::new(Span::call_site(),"yikes!").into_token_stream(self) becomes compile_error!("yikes!") which provides better info to the user (that's you!) than when serializing it to file, since the provided span for the syn::Error is printed differently - being pointed to the compile_error! invocation in the generated file is not helpful, and rustc can point to the span instead.

rustfmt-free formatting: pretty

When built with feature pretty, the output is formatted with prettier-please. Note that this adds additional compiletime overhead and weight to the crate as a trade off not needing any host side tooling.

The formatting output will, for any significant amount of lines of code, differ from the output of rustfmt.